﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using mshtml;
using SpeechLib;
using System.Windows.Forms;

namespace XinChen
{
    class ms_Speech
    {
        private static int instance = 0;//计算实例化的总数量

        public event EventHandler SpeeckDone;
        public event VisemeEventHandler Viseme;
        /// <summary>
        /// Viseme事件参数，提供当前音和下一个音，有效范围[0-21]
        /// </summary>
        public class VisemeEventArgs : EventArgs
        {
            public int CurrentID { get; protected set; }
            public int NextID { get; protected set; }
            public VisemeEventArgs(SpeechVisemeType CurrentVisemeId, SpeechVisemeType NextVisemeId)
            {
                CurrentID = (int)CurrentVisemeId;
                NextID = (int)NextVisemeId;
            }
        }
        public delegate void VisemeEventHandler(object sender, VisemeEventArgs e);


        //public
        /// <summary>
        /// 可用TTS音库列表
        /// </summary>
        public string[] VoiceNames
        {
            get;
            protected set;
        }
        /// <summary>
        /// 当前音库名称
        /// </summary>
        public string Voice
        {
            get { return GetVoice(); }
            set { SetVoice(value); }
        }
        /// <summary>
        /// 是否激活跳字功能，注意StartSpeak的调用方法
        /// </summary>
        public bool HighLight
        {
            get;
            protected set;
        }
        /// <summary>
        /// 创建实例是否成功，目前仅受instance数量限制
        /// </summary>
        public bool Ready
        {
            get;
            protected set;
        }
        /// <summary>
        /// 朗读速度，有效区间[-10,10]
        /// </summary>
        public int Rate
        {
            get { return _SpeechVoice.Rate; }
            set { if (value >= -10 && value <= 10) { _SpeechVoice.Rate = value; } }
        }

        //private
        private SpVoice _SpeechVoice;
        private ISpeechObjectTokens _SpeechVoice_List;
        private SpeechVoiceSpeakFlags _SpeechVoice_Flags;

        //朗读文本相关参数
        private string _text2Read;
        //用于WebBrowser跳字
        private WebBrowser _webBox2Highlight;
        private IHTMLTxtRange _webBox_SelRange;
        private IHTMLTxtRange _webBox_LeftRange;
        //用于TextBox跳字
        private TextBox _textBox2Highlight;
        private int _txtBox_StartOffset;

        /// <summary>
        /// 静态方法，用于快速调用朗读（异步）
        /// </summary>
        /// <param name="text2read">朗读的文本，支持XML（标签参阅：http://msdn.microsoft.com/en-us/library/ee431815(VS.85).aspx）</param>
        public static void Speak(string text2read)
        {
            SpVoice SpeechVoice = new SpVoice();
            SpeechVoice.AllowAudioOutputFormatChangesOnNextSet = false;//防止沙声
            SpeechVoice.Speak(text2read, SpeechVoiceSpeakFlags.SVSFDefault | SpeechVoiceSpeakFlags.SVSFlagsAsync | SpeechVoiceSpeakFlags.SVSFIsXML);
        }

        /* 在Win2003下有时会失败，原因未明
        /// <summary>
        /// 静态方法，将文本输出成声音文件，wav格式。
        /// 建议用XML标签来设置朗读参数，再通过异步调用完成大段文章的输出，最后再用XinChen.Audio类合成。
        /// </summary>
        /// <param name="text2read">朗读的文本，支持XML（标签参阅：http://msdn.microsoft.com/en-us/library/ee431815(VS.85).aspx）</param>
        /// <param name="filename">文件名，不检查扩展名，输出成wav</param>
        public static bool Speak2File(string text2read, string filename)
        {
            SpVoice SpeechVoice = new SpVoice();
            SpeechVoice.AllowAudioOutputFormatChangesOnNextSet = false;//防止沙声

            SpFileStream SpFile = new SpFileStream();
            SpFile.Format.Type = SpeechAudioFormatType.SAFT16kHz16BitStereo;//CQQ说这是唯一支持的格式

            try
            {
                SpFile.Open(filename, SpeechStreamFileMode.SSFMCreateForWrite, false);

                SpeechVoice.AudioOutputStream = SpFile;
                SpeechVoice.Speak(text2read, SpeechVoiceSpeakFlags.SVSFDefault | SpeechVoiceSpeakFlags.SVSFlagsAsync | SpeechVoiceSpeakFlags.SVSFIsXML);

                SpeechVoice.WaitUntilDone(System.Threading.Timeout.Infinite);
                SpFile.Close();
                SpeechVoice.AudioOutputStream = null;

                return true;
            }
            catch
            {
                //有可能因为打开文件失败导致
                return false;
            }
        }*/

        /// <summary>
        /// 将文本输出成声音文件，wav格式。
        /// 建议用XML标签来设置朗读参数，再通过异步调用完成大段文章的输出，最后再用XinChen.Audio类合成。
        /// </summary>
        /// <param name="text2read">朗读的文本，支持XML（标签参阅：http://msdn.microsoft.com/en-us/library/ee431815(VS.85).aspx）</param>
        /// <param name="filename">文件名，不检查扩展名，输出成wav</param>
        public bool Speak2File(string text2read, string filename)
        {
            SpFileStream SpFile = new SpFileStream();
            //SpFile.Format.Type = SpeechAudioFormatType.SAFT16kHz16BitStereo;//CQQ说这是唯一支持的格式
            SpFile.Format.Type = SpeechAudioFormatType.SAFT32kHz16BitStereo;//我试试更高的格式

            try
            {
                SpFile.Open(filename, SpeechStreamFileMode.SSFMCreateForWrite, false);
                _SpeechVoice.AudioOutputStream = SpFile;
                StartSpeak(text2read);

                _SpeechVoice.WaitUntilDone(System.Threading.Timeout.Infinite);
                SpFile.Close();
                _SpeechVoice.AudioOutputStream = null;

                return true;
            }
            catch
            {
                //有可能因为打开文件失败导致
                return false;
            }
        }

        public ms_Speech()
        {
            ms_Speech.instance++;
            if (ms_Speech.instance > 10) //控制不能超过10个实例，仅作预留控制
            {
                Ready = false;
                return;
            }

            _SpeechVoice = new SpVoice();
            _SpeechVoice_Flags = SpeechVoiceSpeakFlags.SVSFDefault | SpeechVoiceSpeakFlags.SVSFlagsAsync | SpeechVoiceSpeakFlags.SVSFIsXML;
            _SpeechVoice.AllowAudioOutputFormatChangesOnNextSet = false;//防止沙声

            _SpeechVoice_List = _SpeechVoice.GetVoices("", "");
            VoiceNames = new string[_SpeechVoice_List.Count];
            for (int i = 0; i < _SpeechVoice_List.Count; i++)
            {
                VoiceNames[i] = _SpeechVoice_List.Item(i).GetAttribute("Name");
            }

            _SpeechVoice.Word += new _ISpeechVoiceEvents_WordEventHandler(do_HighLightSpokenWord);//跳字
            _SpeechVoice.EndStream += new _ISpeechVoiceEvents_EndStreamEventHandler(do_SpeechDone);//结束事件
            _SpeechVoice.Viseme += new _ISpeechVoiceEvents_VisemeEventHandler(do_Viseme);//发音嘴型处理

            //根据不同语种的每一个音节处理
            //_SpeechVoice.Phoneme += new _ISpeechVoiceEvents_PhonemeEventHandler(Phoneme);
            //ttsPhoneConverter = new SpPhoneConverter();
            //ttsPhoneConverter.LanguageId = 1033;//1033 English,2052 简体中文

            Reset();
            Ready = true;
        }
        ~ms_Speech()
        {
            if (Ready)
            {
                ms_Speech.instance--;
            }
        }

        #region 朗读设置
        /// <summary>
        /// 选择TTS音库
        /// </summary>
        /// <param name="VoiceName">从VoiceNames属性中选择一个音库名称</param>
        /// <returns>成功设置音库</returns>
        public bool SetVoice(string VoiceName)
        {
            if (_SpeechVoice.Voice.GetAttribute("Name") != VoiceName)
            {
                for (int i = 0; i < VoiceNames.Length; i++)
                {
                    if (VoiceNames[i] == VoiceName)
                    {
                        _SpeechVoice.Voice = _SpeechVoice_List.Item(i);
                        break;
                    }
                }

                ////另一种直接设置的方法，音库数量少的时候似乎上面的方法更有效率
                //ISpeechObjectTokens tmpVoices = _SpeechVoice.GetVoices("Name=" + VoiceName, "");
                //if (tmpVoices.Count > 0)
                //{
                //    _SpeechVoice.Voice = tmpVoices.Item(0);
                //}
            }
            return _SpeechVoice.Voice.GetAttribute("Name") == VoiceName;
        }
        /// <summary>
        /// 获取当前TTS音库名称
        /// </summary>
        /// <returns>TTS音库名称</returns>
        public string GetVoice()
        {
            return _SpeechVoice.Voice.GetAttribute("Name");
        }
        #endregion

        #region 朗读功能
        /// <summary>
        /// 使用当前设置朗读文本（异步），不需要跳字
        /// </summary>
        /// <param name="text2read">朗读的文本，支持XML</param>
        public void StartSpeak(string text2read)
        {
            if (_SpeechVoice.Status.RunningState == SpeechRunState.SRSEIsSpeaking || _SpeechVoice.Status.RunningState == 0)
            {
                StopSpeak();
            }
            _text2Read = text2read;
            _SpeechVoice.Speak(_text2Read, _SpeechVoice_Flags);
        }

        #region 朗读WebBrowser
        /// <summary>
        /// 朗读指定WebBrowser控件内选定的区域，并实现跳字功能（如无需跳字，请直接调用StartSpeak）
        /// </summary>
        /// <param name="webBox2read">提供朗读内容和跳字显示的WebBrowser控件</param>
        /// <param name="selRange">指定朗读文本范围</param>
        public void StartSpeak(WebBrowser webBox2read, IHTMLTxtRange selRange)
        {
            if (_SpeechVoice.Status.RunningState == SpeechRunState.SRSEIsSpeaking || _SpeechVoice.Status.RunningState == 0)
            {
                StopSpeak();
            }

            if (selRange.text == null)
            {
                //MessageBox.Show("nothing to read!");
                do_SpeechDone(0, null);
                return;
            }
            //扩展头尾至完整单词
            selRange.expand("word");
            selRange.select();
            //保存选区前面的区域，用于跳字时的偏移
            _webBox_LeftRange = selRange.duplicate();
            _webBox_LeftRange.expand("textedit");
            _webBox_LeftRange.setEndPoint("EndToStart", selRange);

            HighLight = true;
            _webBox2Highlight = webBox2read;
            _webBox_SelRange = selRange.duplicate();
            StartSpeak(selRange.text);
        }
        /// <summary>
        /// 朗读指定WebBrowser控件内全部内容，并实现跳字功能（如无需跳字，请直接调用StartSpeak）
        /// </summary>
        /// <param name="webBox2read">提供朗读内容和跳字显示的WebBrowser控件</param>
        public void StartSpeak(WebBrowser webBox2read)
        {
            webBox2read.Document.ExecCommand("SelectAll", false, null);
            StartSpeak(webBox2read, ((webBox2read.Document.DomDocument as IHTMLDocument2).selection.createRange() as IHTMLTxtRange));
        }
        #endregion

        #region 朗读TextBox
        /// <summary>
        /// 朗读指定TextBox控件内全部内容，并实现跳字功能（如无需跳字，请直接调用StartSpeak）
        /// </summary>
        /// <param name="txtBox2Read">提供朗读内容和跳字显示的TextBox控件</param>
        public void StartSpeak(TextBox txtBox2Read)
        {
            StartSpeak(txtBox2Read, 0, txtBox2Read.Text);
        }
        /// <summary>
        /// 朗读指定TextBox控件内选定的区域，并实现跳字功能（如无需跳字，请直接调用StartSpeak）
        /// </summary>
        /// <param name="txtBox2Read">提供朗读内容和跳字显示的TextBox控件</param>
        /// <param name="StartPos">开始朗读位置</param>
        /// <param name="Length">朗读文本长度</param>
        public void StartSpeak(TextBox txtBox2Read, int StartPos, int Length)
        {
            if (StartPos < 0 || Length <= 0 || txtBox2Read.Text.Length < StartPos + Length)
            {
                //MessageBox.Show("nothing to read!");
                do_SpeechDone(0, null);
                return;
            }
            StartSpeak(txtBox2Read, StartPos, txtBox2Read.Text.Substring(StartPos, Length));
        }
        /// <summary>
        /// 朗读特定文本，并在指定TextBox实现跳字功能（如无需跳字，请直接调用StartSpeak）
        /// </summary>
        /// <param name="txtBox2Highlight">提供跳字显示的TextBox控件</param>
        /// <param name="StartPos">TextBox控件开始跳字位置</param>
        /// <param name="txt2Read">实际朗读的文本，应与TextBox控件高亮文本长度一致</param>
        public void StartSpeak(TextBox txtBox2Highlight, int StartPos, string txt2Read)
        {
            if (_SpeechVoice.Status.RunningState == SpeechRunState.SRSEIsSpeaking || _SpeechVoice.Status.RunningState == 0)
            {
                StopSpeak();
            }

            if (StartPos < 0 || txtBox2Highlight.Text.Length < StartPos + txt2Read.Length)
            {
                //MessageBox.Show("nothing to read!");
                do_SpeechDone(0, null);
                return;
            }

            HighLight = true;
            _textBox2Highlight = txtBox2Highlight;
            _txtBox_StartOffset = StartPos;
            _textBox2Highlight.HideSelection = false;//确保控件失去焦点时仍能正常显示高亮文本
            StartSpeak(txt2Read);
        }
        #endregion

        /// <summary>
        /// 停止所有朗读，并复位高亮设置
        /// </summary>
        public void StopSpeak()
        {
            _SpeechVoice.Speak(string.Empty, SpeechVoiceSpeakFlags.SVSFPurgeBeforeSpeak);//Stop the voice
            Reset();
        }
        /// <summary>
        /// 还原高亮设置（清理控件引用，清空朗读文本，HighLight = false）
        /// </summary>
        private void Reset()
        {
            HighLight = false;
            _text2Read = null;
            _webBox2Highlight = null;
            _textBox2Highlight = null;
        }
        #endregion

        #region TTS事件处理
        void do_HighLightSpokenWord(int StreamNumber, object StreamPosition, int CharacterPosition, int Length)
        {
            if (!HighLight) { return; }

            if (_webBox2Highlight != null)
            {
                IHTMLTxtRange rng2HighLight = _webBox_SelRange.duplicate();
                //先将光标移到偏移区域的末尾
                rng2HighLight.setEndPoint("StartToEnd", _webBox_LeftRange);
                rng2HighLight.setEndPoint("EndToEnd", _webBox_LeftRange);
                //rng2HighLight.select();

                ////第一个方法，按TTS返回的位置，将光标移到指定区域
                //rng2HighLight.moveStart("character", CharacterPosition);
                //rng2HighLight.moveEnd("character", Length);
                //rng2HighLight.select();
                //System.Diagnostics.Debug.WriteLine(CharacterPosition.ToString() + "," + Length.ToString());
                //System.Diagnostics.Debug.WriteLine(_text2Read.Substring(CharacterPosition, Length) + "[" + rng2HighLight.text + "]");

                //发现TTS返回的位置跟控件上位置不符，用另一个方法
                //开始朗读后一直向前搜索当前单词，不往回跳，如果找不到就跳过这个单词不高亮。
                //如果在听写中，朗读出当前位置不存在的单词，有可能会跳到后面去，然后就一直不同步，因为不能回跳。
                //所以要用听写功能必须保证所有挖空单词都不存在。不过听写时跳字是不是有点过分？怎么学生填空啊？
                if (rng2HighLight.findText(_text2Read.Substring(CharacterPosition, Length), _text2Read.Length - CharacterPosition, 6))
                {
                    rng2HighLight.select();
                    _webBox_LeftRange.setEndPoint("EndToEnd", rng2HighLight);//更新偏移区域
                }
            }

            if (_textBox2Highlight != null)
            {
                _textBox2Highlight.SelectionStart = CharacterPosition + _txtBox_StartOffset;
                _textBox2Highlight.SelectionLength = Length;
            }
        }
        void do_SpeechDone(int StreamNumber, object StreamPosition)
        {
            if (SpeeckDone != null) { SpeeckDone(this, new EventArgs()); }
        }
        void do_Viseme(int StreamNumber, object StreamPosition, int Duration, SpeechVisemeType NextVisemeId, SpeechVisemeFeature Feature, SpeechVisemeType CurrentVisemeId)
        {
            //http://msdn.microsoft.com/en-us/library/ee125219(v=VS.85).aspx
            if (Viseme != null)
            {
                Viseme(this, new VisemeEventArgs(CurrentVisemeId, NextVisemeId));
            }
        }
        void do_Phoneme(int StreamNumber, object StreamPosition, int Duration, short NextPhoneId, SpeechVisemeFeature Feature, short CurrentPhoneId)
        {
            //http://msdn.microsoft.com/en-us/library/ee431828(v=VS.85).aspx
            //http://msdn.microsoft.com/en-us/library/ee125160(v=VS.85).aspx
            //System.Diagnostics.Debug.Write(ttsPhoneConverter.IdToPhone(CurrentPhoneId) + ":");
            //if (ttsPhoneConverter.IdToPhone(NextPhoneId)=="_")
            //{
            //    System.Diagnostics.Debug.WriteLine(" ");
            //}
        }
        #endregion
    }
}